<?php

namespace mForge\Modules\Mail;

/**
 * The Mail class represents an email.
 */
class Mail
{
    const TO = 'To';
    const FROM = 'From';
    const BCC = 'Bcc';
    const CC = 'Cc';
    const REPLY_TO = 'Reply-To';
    const SENDER = 'Sender';
    const SUBJECT = 'Subject';
    const RETURN_PATH = 'Return-Path';

    /**
     *
     * @var string
     */
    private $boundary;

    /**
     * Mail headers.
     *
     * @var array
     */
    private $headers = array();

    /**
     * Any MIME parts.
     *
     * @var string
     */
    private $mimeParts;

    /**
     *
     * @var string
     */
    private $text;

    /**
     *
     */
    public function __construct()
    {
        $this->boundary = md5(time());
    }

    /**
     *
     * @param MimePart $mimePart
     * @return Mail
     */
    public function addPart(MimePart $mimePart)
    {
        $this->mimeParts[] = $mimePart;
        return $this;
    }

    /**
     *
     * @param string $data
     * @param string $contentType
     * @param string $filename
     * @return Mail
     */
    public function addFileAttachment($data, $contentType, $filename)
    {
        $mimePart = new MimePart(chunk_split(base64_encode($data)), $contentType . "; name=\"$filename\"", 'base64');
        $mimePart->setContentDisposition("attachment; filename=\"$filename\"");

        $this->mimeParts[] = $mimePart;

        return $this;
    }

    /**
     * Adds a BCC to the mail.
     *
     * @param string $bcc
     * @param string $bccName
     * @return Mail
     */
    public function addBcc($bcc, $bccName = null)
    {
        if (!is_array($bcc)) {
            $bcc = array($bcc => $bccName);
        } else if (!\mForge\Utility::isArrayAssociative($bcc)) {
            $bcc = array_combine($bcc, array_fill(0, count($bcc), null));
        }
        $current = isset($this->headers[self::BCC]) ? $this->headers[self::BCC] : array();
        $this->headers[self::BCC] = array_merge($current, $bcc);
        return $this;
    }

    /**
     * Adds a CC to the mail.
     *
     * @param string $cc
     * @param string $ccName
     * @return Mail
     */
    public function addCc($cc, $ccName = null)
    {
        if (!is_array($cc)) {
            $cc = array($cc => $ccName);
        } else if (!\mForge\Utility::isArrayAssociative($cc)) {
            $cc = array_combine($cc, array_fill(0, count($cc), null));
        }
        $current = isset($this->headers[self::CC]) ? $this->headers[self::CC] : array();
        $this->headers[self::CC] = array_merge($current, $cc);
        return $this;
    }

    /**
     * Adds a mail recipient.
     *
     * @param string $to
     * @param string $toName
     * @return Mail
     */
    public function addTo($to, $toName = null)
    {
        if (!is_array($to)) {
            $to = array($to => $toName);
        } else if (!\mForge\Utility::isArrayAssociative($to)) {
            $to = array_combine($to, array_fill(0, count($to), null));
        }
        $current = isset($this->headers[self::TO]) ? $this->headers[self::TO] : array();
        $this->headers[self::TO] = array_merge($current, $to);
        return $this;
    }

    /**
     * Returns the mail body.
     *
     * @return string
     */
    public function getBody()
    {
       if (count($this->mimeParts)) {

           $message = '';
           if (!empty($this->text)) {
               $message .= '--' . $this->boundary . PHP_EOL;
               $message .= 'Content-Type: text/plain; charset=UTF-8' . PHP_EOL . PHP_EOL;
               $message .= $this->text . PHP_EOL . PHP_EOL;
           }

           foreach ($this->mimeParts as $part) {
               $message .= '--' . $this->boundary . PHP_EOL;
               $message .= 'Content-Type: ' . $part->getContentType() . '; charset=' . $part->getCharset() . PHP_EOL;
               $message .= 'Content-Disposition: ' . $part->getContentDisposition() . PHP_EOL;
               $message .= 'Content-Transfer-Encoding: ' . $part->getTransferEncoding() . PHP_EOL . PHP_EOL;
               $message .= $part->getData() . PHP_EOL . PHP_EOL;
           }

           $message .= '--' . $this->boundary . '--' . PHP_EOL;

           return $message;
       }
       return $this->text;
    }

    /**
     * Returns the From header if $formatted set to true, else the from mail
     * address will be returned.
     *
     * @return string
     */
    public function getFrom($formatted = true)
    {
        return $this->getHeader(self::FROM, $formatted);
    }

    /**
     * Returns the specified header.
     *
     * @param string $header
     * @return string
     */
    public function getHeader($header, $formatted = true)
    {
        if (!$formatted) {
            return isset($this->headers[$header]) ? $this->headers[$header] : null;
        }
        if (!isset($this->headers[$header])) {
            throw new \Exception("The header '$header' is not set.");
        }
        $value = $this->headers[$header];
        if (is_array($value)) {
            $addresses = $value;
            $value = '';
            foreach ($addresses as $email => $name) {
                if (empty ($name)) {
                    $value .= $email . ', ';
                } else {
                    $value .= $name . ' <' . $email . '>, ';
                }
            }
            $value = rtrim($value, ', ');
//            TODO: check that values are no more than 998 chars long / or break line at < 78 chars
        }
        return $value;
    }

    /**
     * Returns all headers.
     *
     * @return array
     */
    public function getHeaders($formatted = true, array $omitted = null)
    {
        if (count($this->mimeParts)) {
           $this->headers['MIME-Version'] = '1.0';
           $this->headers['Content-Type'] = 'multipart/alternative; boundary="' . $this->boundary . '"';
        }

        $headers = $this->headers;
        if ($omitted !== null) {
            foreach ($omitted as $omittedHeader) {
                unset($headers[$omittedHeader]);
            }
        }

        if ($formatted) {
            $rfc2822 = '';
            foreach ($headers as $header => $value) {
                $rfc2822 .=  $header . ': ' . $this->getHeader($header) . PHP_EOL;
            }
            return $rfc2822;
        }
        return $headers;
    }

    /**
     * Returns the Sender header.
     *
     * @return string
     */
    public function getSender($formatted = true)
    {
        return $this->getHeader(self::SENDER, $formatted);
    }

    /**
     * Returns the subject.
     *
     * @return string
     */
    public function getSubject($formatted = true)
    {
        return $this->getHeader(self::SUBJECT, $formatted);
    }

    /**
     * Returns the To header.
     *
     * @return string
     */
    public function getTo($formatted = true)
    {
        return $this->getHeader(self::TO, $formatted);
    }

    /**
     * Sets BCC('s).
     *
     * @param string|array $bcc
     * @return Mail
     */
    public function setBcc($bcc, $bccName = null)
    {
        if (!is_array($bcc)) {
            $bcc = array($bcc => $bccName);
        } else if (!\mForge\Utility::isArrayAssociative($bcc)) {
            $bcc = array_combine($bcc, array_fill(0, count($bcc), null));
        }
        return $this->setHeader(self::BCC, $bcc);
    }

    /**
     * Sets CC('s).
     *
     * @param string|array $cc
     * @return Mail
     */
    public function setCc($cc, $ccName = null)
    {
        if (!is_array($cc)) {
            $cc = array($cc => $ccName);
        } else if (!\mForge\Utility::isArrayAssociative($cc)) {
            $cc = array_combine($cc, array_fill(0, count($cc), null));
        }
        return $this->setHeader(self::CC, $cc);
    }

    /**
     * Sets the From header.
     *
     * @param string $from
     * @return Mail
     */
    public function setFrom($from, $fromName = null)
    {
        return $this->setHeader(self::FROM, array($from => $fromName));
    }

    /**
     * Sets a header.
     *
     * @param string $header
     * @param string $value
     * @return Mail
     */
    public function setHeader($header, $value)
    {
        if (preg_match('/\s/', $header)) {
            throw new \InvalidArgumentException('The header key may not contain whitespace.');
        }
        $this->headers[$header] = $value;
        return $this;
    }

    /**
     *
     * @param string $html
     */
    public function setHtml($html)
    {
        $this->mimeParts['html'] = new MimePart($html, 'text/html');
        return $this;
    }

    /**
     * Sets the Reply-To header.
     *
     * @param string $replyTo
     * @return Mail
     */
    public function setReplyTo($replyTo)
    {
        return $this->setHeader(self::REPLY_TO, $replyTo);
    }

    /**
     * Sets the Return-Path header.
     *
     * @param string $returnPath
     * @return Mail
     */
    public function setReturnPath($returnPath)
    {
        return $this->setHeader(self::RETURN_PATH, $returnPath);
    }

    /**
     * Sets the Sender header.
     *
     * @param string $sender
     * @return Mail
     */
    public function setSender($sender)
    {
        return $this->setHeader(self::SENDER, $sender);
    }

    /**
     * Sets the Subject header.
     *
     * @param string $subject
     * @return Mail
     */
    public function setSubject($subject)
    {
        return $this->setHeader(self::SUBJECT, $subject);
    }

    /**
     * Sets the mail body as text.
     *
     * @param string $text
     * @return Mail
     */
    public function setText($text)
    {
        $this->text .= wordwrap($text, 70);
        return $this;
    }

    /**
     * Sets the To header.
     *
     * @param string|array $to
     * @return Mail
     */
    public function setTo($to, $toName = null) {
        if (!is_array($to)) {
            $to = array($to => $toName);
        } else if (!\mForge\Utility::isArrayAssociative($to)) {
            $to = array_combine($to, array_fill(0, count($to), null));
        }
        $this->setHeader(self::TO, $to);
        return $this;
    }

}